Creating a custom reusable filtering interface with macro component: An Example
Neil Lee, Engineer, Potix Corporation
April 23, 2015
ZK 7.0.5
Introduction
Filtering commonly augments data displaying functionality. Users can create multiple views of a single data set based on different filter criteria. As data sets become larger and larger, filtering also helps increase performance and improve usability by reducing the information shown.
Application authors would need to design an user interface to allow users to select their filtering preferences. Take Excel for example. After filtering is enabled, each column header will include a button, such that when clicked, displays the filtering configuration dialog box for the respective column. Users can then edit the condition that the data in that column must satisfy in order to be included in the final display.
This smalltalk illustrates the process of adding filtering function to a data display component via a simple example. Filtering function can be divided into 3 elements: user interface, filter definition, and the actual filtering implementation. First, an user interface is required for collecting filtering criteria from users. Then, the gathered filter definition can be used by the application to retrieve requested data from the service layer, and update the data display accordingly. For simplicity, I assume the original dataset is small enough to fit into memory entirely, and provide a sample implementation for in-memory filtering. It is important to note that the actual filtering operation happens not in the UI, but at service level. This enables the same UI to be reused with a different service implementation. Conversely, developers can also provide a different UI for the same service implementation. Likewise, filter definition may also be reused or customized without affecting the other two filtering elements.
Live Demo
As an example, law enforcement agencies can use filtering to find trends in criminal activities. For concreteness, criminal records were taken from Sacramento in 2006 (see data source here), which consists of 7,584 offenses. It is also interesting to mark illegal occurrences on google maps.
Please watch the video below to see the application in action.
How it works
In the demo, I use a listbox to display the criminal records in a table. Each column heading contains a button with a filter icon. That button, together with the popup dialog box, is encapsulated into a macro component called FilterConfig (see the image below) for ease of reuse. This dialog box allows the user to create a filter rule (e.g. contains, begins with, less than, ...) with its associated parameter, or to choose values from an optional distinct values set. The user could also remove the filter rule for the corresponding column. Moreover, the dialog box comes with two custom events for an application to handle filter application and removal.
Filter rules are dependent on the data type. For numerical data, the available rules could be "less than", "equal to", "greater than", etc. For textual data, the available rules could include "begins with", "ends with", and "contains...". Since you, as the developer of the the service layer, know about the data the application tries to filter, the application should obtain the available filter models from the service.
User Interface
The FilterConfig macro component can be inserted in the list header. It requires a FilterModel object to keep track of the current filter configuration in the dialog. The component will also notify the application when filter configuration changes or clears.
<listheader>
<filterConfig
model="..."
onFilterChanged="..."
onFilterCleared="...".../>
</listheader>
The macro component provides a default template for filter config user interface. Developers can also define their own template, and specify it via popupTemplate argument.
<template name="custom">
...
</template>
...
<filterConfig
popupTemplate="custom" .../>
Application
After the user interface for filtering operation is constructed, application can follow the steps below to perform filtering life-cycle.
- Retrieve available filter models from data service
- Each filterConfig component is driven by a filter model. Since the filter model depends on the data, the application should retrieve available filter models from the data service responsible for retrieving domain data from the data source.
private Map<String, FilterModel<?>> availableFilterModels = null;
...
@Init
public void init() {
// prepare model for unfiltered data
availableFilterModels = service.getAvailableFilterModels();
filterData();
}
- Render filter dialog based on filter model
- The filter model contains enough information to fill available filter rule dropdown list, and optionally render the distinct value list.
public interface FilterModel<E> {
public abstract String getFilterId();
public abstract List<String> getRuleNames();
public abstract String getType();
public abstract ListModelList<E> getDistinctValues();
public abstract FilterRule getRule();
}
- Listens to onFilterChanged and onFilterCleared events
- The application can then update the list of active filters upon receiving these two events
@Command("applyFilter")
@NotifyChange({"crimeRecords", "captionLabel"})
public void applyFilter(@BindingParam("model") FilterModelImpl<?> model) {
activeFilterModels.add(model);
filterData();
}
- Retrieve filtered data from data service based on user choices.
- Given the set of active filters, application can ask the data service to pick out the data items satisfying all of them.
private void filterData() {
crimeRecords.clear();
List<CrimeRecord> findRecords = service.findRecords(activeFilterModels, LIMIT);
for (CrimeRecord crimeRecord : findRecords) {
crimeRecords.add(new UiCrimeRecord(crimeRecord));
}
updateCaption();
}
- Replace list model to update the UI
- Once the filtered data is obtained, the list model driven the listbox is updated to contain the new data. Then, the listbox is automatically updated to show the filtered data.
private void filterData() {
crimeRecords.clear();
List<CrimeRecord> findRecords = service.findRecords(activeFilterModels, LIMIT);
for (CrimeRecord crimeRecord : findRecords) {
crimeRecords.add(new UiCrimeRecord(crimeRecord));
}
updateCaption();
}
Service
Enterprise applications often organize common functionalities into services. Separating data access operations into a data service isolates the rest of the application from the specific database engines or storage technologies. Advantages include easier migration to other database engines, better encapsulation of data access logic which facilatates reuse in future projects, etc. Since filtering can be described as conditional data access, the data service should be the place to add the actual filtering logic.
- Provides available filter options
public Map<String, FilterModel<?>> getAvailableFilterModels() {
Map<String, FilterModel<?>> availableFilterModels = new LinkedHashMap<String, FilterModel<?>>();
// could be cached
List<?> completeList = dao.getRecords();
Set<Integer> distinctDistricts =
MemoryFilterUtils.<Integer> getDistinctValues(completeList, "district");
Set<String> distinctDescriptions = MemoryFilterUtils
.<String> getDistinctValues(completeList, "description");
// consider dynamic creation based on reflection
availableFilterModels
.put("address", new FilterModelImpl<String>("address", "string", null));
availableFilterModels
.put("district", new FilterModelImpl<Integer>("district", "number", distinctDistricts));
availableFilterModels
.put("description", new FilterModelImpl<String>("description", "string", distinctDescriptions));
availableFilterModels
.put("latitude", new FilterModelImpl<Number>("latitude", "number", null));
availableFilterModels
.put("longitude", new FilterModelImpl<Number>("longitude", "number", null));
return availableFilterModels;
}
- Filter data based on filter configuration
- Given the set of active filters, the data service can then pick out the data items satisfying all of them. For simplicity, the demo provides in-memory filtering implementation. Generally, the combined filter rules will be used to create a query command to retrieve data from a database (e.g. WHERE clause of a SQL Select).
public List<CrimeRecord> findRecords(Set<FilterModel<?>> filterModels, int limit) {
// instead of memory filtering, e.g. create a dynamic database query, or
// call your dao
List<CrimeRecord> completeList = dao.getRecords();
List<CrimeRecord> filteredList = new ArrayList<CrimeRecord>(completeList);
Predicate combinedPredicate = MemoryFilterUtils.createCombinedFilterPredicate(filterModels);
CollectionUtils.filter(filteredList, combinedPredicate);
return filteredList.subList(0, Math.min(filteredList.size(), limit));
}
Summary
This article goes through the process of adding filtering functionality to a simple application. ZK helps with building an user-interface for gathering filter criteria from users, and communicating with the backend data service to retrieve desired data. The application can then work with the updated data, such as displaying it in a listbox.
Separating the filtering UI from the actual filtering operation allows reusing the same user interface, but replacing the filtering action with a different implementation. In ZK, one way to facilitate user interface reusage is through macro components. It is also easier to redesign the view while keeping the underlying service the same.
Download
- The source code for this article can be found in github.
Comments
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |